package cn.sciento.boot.data.change.helper;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.ItemsList;
import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectBody;
import net.sf.jsqlparser.statement.select.SelectItem;
import net.sf.jsqlparser.statement.select.SetOperationList;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.session.RowBounds;
import cn.sciento.boot.data.change.dto.MybatisInvocation;
import cn.sciento.boot.data.change.utils.FieldUtil;
import cn.sciento.boot.data.change.utils.JsqlParserUtil;
import cn.sciento.boot.data.change.utils.MappedStatementUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class KeyGeneratorHelper {
  private static final Logger LOGGER = LoggerFactory.getLogger(KeyGeneratorHelper.class);
  
  private static final String splitStr = "\\.";
  
  public static List<Object> findKeyValuesFromParameter(String keyPropertyForGenerator, Object parameterObject) {
    List<Object> keyValues = new ArrayList();
    if (parameterObject == null)
      return keyValues; 
    Collection<Object> parameters = getParameters(parameterObject);
    String[] keyPath = keyPropertyForGenerator.split("\\.");
    for (Object parameter : parameters) {
      Object value = findKeyValueByKeyPath(keyPath, parameter);
      if (value != null)
        keyValues.add(value); 
    } 
    return keyValues;
  }
  
  public static List<Object> findKeyValuesFromSql(MybatisInvocation mybatisInvocation) {
    Insert insertStatement = (Insert)mybatisInvocation.getSqlParserInfo().getStatement();
    String keyColumn = mybatisInvocation.getTable().getKeyColumns()[0];
    List<Object> keyValues = new ArrayList();
    if (insertStatement == null)
      return keyValues; 
    List<Column> columnList = insertStatement.getColumns();
    if (CollectionUtils.isEmpty(columnList))
      return keyValues; 
    int indexOfKey = indexOfKeyColumn(columnList, keyColumn).intValue();
    if (indexOfKey == -1)
      return keyValues; 
    if (insertStatement.getItemsList() != null) {
      List<ExpressionList> expressionLists = new ArrayList<>();
      ItemsList itemsList = insertStatement.getItemsList();
      if (itemsList instanceof ExpressionList) {
        expressionLists.add((ExpressionList)itemsList);
      } else if (itemsList instanceof MultiExpressionList) {
        expressionLists.addAll(((MultiExpressionList)itemsList).getExprList());
      } else {
        return keyValues;
      } 
      for (ExpressionList expressionList : expressionLists) {
        Expression expression = expressionList.getExpressions().get(indexOfKey);
        if (expression != null)
          keyValues.add(expression.toString()); 
      } 
    } else if (insertStatement.getSelect() != null) {
      ArrayList<Map<String, Object>> querySelectData = querySelectData(mybatisInvocation, indexOfKey);
      if (CollectionUtils.isNotEmpty(querySelectData))
        for (Map<String, Object> resultMap : querySelectData) {
          Object keyValue = resultMap.values().toArray()[0];
          keyValues.add(keyValue);
        }  
    } 
    return keyValues;
  }
  
  private static Collection<Object> getParameters(Object parameter) {
    Collection<Object> parameters = null;
    if (parameter instanceof Collection) {
      parameters = (Collection<Object>)parameter;
    } else if (parameter instanceof Map) {
      Map parameterMap = (Map)parameter;
      if (parameterMap.containsKey("collection")) {
        parameters = (Collection<Object>)parameterMap.get("collection");
      } else if (parameterMap.containsKey("list")) {
        parameters = (List)parameterMap.get("list");
      } else if (parameterMap.containsKey("array")) {
        parameters = Arrays.asList((Object[])parameterMap.get("array"));
      } 
    } 
    if (parameters == null) {
      parameters = new ArrayList();
      parameters.add(parameter);
    } 
    return parameters;
  }
  
  private static ArrayList<Map<String, Object>> querySelectData(MybatisInvocation mybatisInvocation, int indexOfKey) {
    Select select = mybatisInvocation.getSqlParserInfo().getSelect();
    SelectBody selectBody = select.getSelectBody();
    reSetSelectItem(selectBody, indexOfKey);
    List<ParameterMapping> parameterMappings = mybatisInvocation.getBoundSql().getParameterMappings();
    BoundSql queryBoundSql = JsqlParserUtil.buildSelectBoundSql(mybatisInvocation, parameterMappings, select.toString());
    MappedStatement selectMappedStatement = MappedStatementUtil.newHashMapMappedStatement(mybatisInvocation.getMappedStatement());
    Object queryResultList = null;
    try {
      queryResultList = mybatisInvocation.getExecutor().query(selectMappedStatement, mybatisInvocation
          .getParameter(), RowBounds.DEFAULT, null, null, queryBoundSql);
    } catch (SQLException e) {
      LOGGER.info("query select data with error when insert value into {}", mybatisInvocation
          .getTable().getName());
    } 
    return (ArrayList<Map<String, Object>>)queryResultList;
  }
  
  private static Integer indexOfKeyColumn(List<Column> columnList, String keyColumn) {
    int index = -1;
    for (Column column : columnList) {
      index++;
      if (column.getColumnName().equals(keyColumn))
        return Integer.valueOf(index); 
    } 
    return Integer.valueOf(-1);
  }
  
  private static void reSetSelectItem(SelectBody selectBody, int indexOfKey) {
    if (selectBody instanceof PlainSelect) {
      PlainSelect plainSelect = (PlainSelect)selectBody;
      List<SelectItem> selectItems = plainSelect.getSelectItems();
      SelectItem selectItem = selectItems.get(indexOfKey);
      selectItems.clear();
      selectItems.add(selectItem);
    } else if (selectBody instanceof SetOperationList) {
      SetOperationList setOperationList = (SetOperationList)selectBody;
      setOperationList.getSelects().forEach(item -> reSetSelectItem(item, indexOfKey));
    } 
  }
  
  private static Object findKeyValueByKeyPath(String[] keyPath, Object parameter) {
    if (keyPath == null || keyPath.length == 0)
      return null; 
    Object parameterValue = parameter;
    for (int i = 0; i < keyPath.length && 
      parameterValue != null; i++) {
      Map<String, Object> objectMap;
      if (parameterValue instanceof Map) {
        objectMap = (Map<String, Object>)parameterValue;
      } else {
        objectMap = FieldUtil.objectToMap(parameterValue);
      } 
      parameterValue = objectMap.get(keyPath[i]);
    } 
    return parameterValue;
  }
}
